Main documentation for the GoodForm Grails plugin version 2.0
- Main documentation
- Tutorial
- Changes
- Compatibility
1. Introduction
GoodForm provides a complete complex form system. Complex forms can be very lengthy, and only ask questions relevant to person filling in the form, dependant on the data entered by the user. The form can be entered over many sessions, data is saved as they progress through form segments, and the user can always see and edit previous sections. If they change an answer, GoodForm re checks the existing answers and asks any additional questions as required. Existing answers are reused as appropriate.
Forms are Versioned, so you can update your form (definition) on the fly while users are filling in the old version of your form. All newly started forms will be the latest version, but forms underway before the update will remain on the old version. Form version and definitions are stored, so you will always be able to relate the information to the correct version of the form.
Forms also have a user identifier that can be checked and manipulated to provide the required security.
When a form is completed you can access the form data document to process the form. This will vary from application to application, but you could break up the data and enter it into your RDBMS, just store the document data as JSON, or even just print out the data.
As mentioned above the form data represents a document, and is stored as a JSON document in the FormInstance Domain Class. This document becomes the canonical form document, like the paper form people would have filled in in the past.
GoodForm out of the box is responsive and works well across all browsers (IE8 and above included) and all mobile browsers we have tested, including safari, chrome, default Android browsers (samsung, HTC, ASUS), and firefox mobile.
See Changes in 2.0.0
2. Quick start
GoodForm is a grails.org/plugin/goodform[grails plugin]. Just include the plugin dependency in your build config:
compile ":goodform:2.0.0"
We recommend you do the Tutorial to get a better idea as to how good form "hangs together". |
To get a GoodForm project going
-
Install One Ring rules engine
Go get it at http://nerderg.com/One+Ring, install it and set the port to 7070
Then add
goodform.rulesEngine.uri = 'http://localhost:7070/rulesEngine'
to your config.groovy -
Add the GoodForm plugin to your project
Add
compile ":goodform:2.0.0"
to your Grails project BuildConfig.groovyYou may also wish to install Simple Suggestions plugin to get autocomplete fields
-
Create a form definition
You can create a form definition anywhere (create a file and store in git then read it in perhaps), BootStrap.groovy is as good a place as any:
import com.nerderg.goodForm.FormDefinition class BootStrap { def formDataService def init = { servletContext -> String jobApplicationDefinition = """ form { question("Job1") { "What is your name?" group: "names", { "Title" text: 10, hint: "e.g. Mr, Mrs, Ms, Miss, Dr", suggest: "title", map: "Title" "Given Names" text: 50, required: true, map: "givenNames" "Last or Family Name" text: 50, required: true, map: "lastName" "Have you been or are you known by any other names?" map: "hasAlias", hint: "e.g. maiden name, previous married name, alias, name at birth",{ "List your other names" listOf: "aliases", { "Other name" text: 50, map: 'alias' "Type of name" text: 40, hint: "e.g maiden name", suggest: "nameType", map: 'aliasType' } } } } question("Job2") { "Contact details" group: "contact", { "Home address" text: 200, map: 'homeAddress' "Postcode" number: 4, hint: "e.g. 2913", map: 'postcode' "Home Phone" phone: 15, map: 'homePhone' "Mobile Phone" phone: 15, map: 'mobilePhone' "Work Phone" phone: 15, map: 'workPhone' } } //add more... } """ if (!FormDefinition.findByName('JobApplication')) { formDataService.createNewFormVersion('JobApplication', jobApplicationDefinition) } } def destroy = { } }
-
Extend FormController for your form.
import com.nerderg.goodForm.FormController class JobApplicationFormController extends FormController { def createForm() { createForm('JobApplication') } }
-
Create some business rules to to control the Form Flow
Save your rules in
$HOME/.OneRing/rules/jobapplication.ruleset
ruleset("JobApplication") { abortOnFail = false rule("First page of questions") { when { true } then { next = ['Job1'] } } } ruleset("JobApplicationJob1") { require(['Job1']) //we need the answers to these questions abortOnFail = true rule("Second page of job application questions") { when { true } then { messages << "Hello $Job1.names.givenNames" next = ['Job2'] } } } ruleset("JobApplicationJob2") { require(['Job1', 'Job2']) //we need the answers to these questions abortOnFail = true rule("Third page of job application questions") { when { Job2.contact.homePhone.startsWith('02') } then { next = ['End'] } otherwise { if(fieldErrors['Job2.contact.homePhone0']) { fieldErrors['Job2.contact.homePhone0'] += "/nHome phone must start with 02" } else { fieldErrors['Job2.contact.homePhone0'] = "Home phone must start with 02" } next = ['Job2'] } } } // add more ...
-
Run One Ring and your project
You should have a jobApplication controller that has a link to create a new form instance click the link and enjoy.
You can download an example project from GitHub: https://github.com/pmcneil/goodform2-tutorial
3. Form definition DSL
3.1. Good Form DSL basics
The basics:
-
The Good Form DSL lets you define questions.
-
A question is a form element.
-
A question can have sub elements that are displayed depending on the state of the first question.
Here is an example question:
question("Name") {
"What is your name?" group: "names", hint: "Name of the person requiring assistance", {
"Title" text: 10, hint: "e.g. Mr, Mrs, Ms, Miss, Dr", suggest: "title", map: 'title'
"Given Names" text: 50, required: true, map: 'givenNames'
"Last or Family Name" text: 50, required: true, map: 'lastName'
"Have you been or are you known by any other names?" map: 'knownByOtherNames',
hint: "e.g. maiden name, previous married name, alias, name at birth", {
"List your other names" listOf: "aliases", {
"Other name" text: 50, map: 'otherName'
"Type of name" text: 40, hint: "e.g maiden name", suggest: "nameType", map: 'otherNameType'
}
}
}
}
Lets look at that DSL:
-
Line 1 defines the question reference as “Name”.
-
Line 2 defines a question “What is your name?”, which is a group of related fields (or sub questions) called Form Elements. We’ve called the group “names” as you can see by the group: “names”. The final attribute on line 2 is hint, which lets us put some hint text next to the question. The brace “{“ at the end of line 2 encloses the contents of the group, or the form elements that are grouped by this question.
-
Line 3 defines the first text input field form element for title. Here you can see it’s a text element that is 10 characters long, has a hint, and maps to the name title. You can also see a suggest attribute that tells the form to look up title suggestions via the [Simple Suggestions plugin](http://nerderg.com/Simple+Suggestions+plugin).
-
Line 6 is a Boolean, or check box question, "Have you been or are you known by any other names?". The contained elements of this question will only be displayed if the check box is ticked.
-
Line 7 is the first contained element of "Have you been or are you known by any other names?" and it defines a listOf: “aliases” element. A listOf element, as the name suggests, creates a list of the enclosed form elements. You can add multiples of the set of form elements contained in a listOf element. If the user has only one element they fill in the displayed text fields, if they have another alias they click an add button on the form which displays a copy of the sub form for the next alias.
The map attribute of a form element provides the reference to the data within the form data map (which contains the form data). To get the title defined in this question from the form data you would use the following groovy code:
String title = formData.Name.names.title
3.2. Standard Form Elements
Good form comes with a set of standard form elements (Note: you can add your own elements). These are the elements defined in the GoodFormService:
form elements
-
text
-
number
-
phone
-
money
-
select
-
attachment
-
date
-
datetime
-
bool
structural elements
-
heading
-
group
-
pick
-
each
-
listOf
Form element attributes generally have defaults meaning you can leave many of them out except mapping and definition. |
You can add non-reserved attributes to your form dsl. For example you could do this
"what is your postcode?" number: 1000..9999, map: 'postcode', postcode: 'Australian'
and check the postcode attribute exists, and check it’s value in Custom field validators.
|
3.2.1. Text
The text form element displays a text input of a defined length, e.g text: 10 which defines a text field 10 characters in length. If the length is longer than 99 then a text area will be used.
-
text: Number max length of field - e.g. 20*. If the size is > than 100 a textarea is used.
-
map: String name of field in data map - e.g. text20
-
hint: String text to display as a hint to the user - e.g. this is a 20 char text
-
preamble: String text to display before the field as an explanation - e.g. This is 20 char text with a pattern match so only letters can be entered
-
pattern: List a list containing a regex and an error message - e.g. [/[A-Za-z ]+/, You can only have letters]
-
sugest: String the name of a suggestion look up via the [Simple Suggestions plugin](http://nerderg.com/Simple+Suggestions+plugin) - e.g. colour
-
required: Boolean is this field required - e.g. true
-
default: String the default value of the field - e.g. hello
question("Example1") {
"Surname" text: 10, map: 'surname'
}
question("Example2") {
"Text input 20?" text: 20,
map: 'text20',
hint: 'this is a 20 char text',
preamble: 'This is 20 char text with a pattern match so only letters can be entered',
pattern: [/[A-Za-z ]+/, 'You can only have letters'],
sugest: 'colour',
required: true,
default: 'hello'
}
//this will show as a textarea by default as it's a large field
question("Example3") {
"Text input 120?" text: 120,
map: 'text120',
required: true,
default: 'hello is it me you are looking for?'
}
3.2.2. Number
The number form element displays a numeric text field. It will be validated as a number. The element is defined with the max number of digits including decimal point,
e.g. number: 5 will be ok for 12345 or 123.5 or 1.256 or -12.3.
A number can also be defined with a range of numbers
e.g. number: [0..20] allowing the numbers 0 to 20 to be entered.
-
number: Number or Range max number of digits including decimal point or a Range of numbers - e.g 5 or 0..100
-
map: String name of field in data map - e.g.'numberRange'
-
hint: String text to display as a hint to the user - e.g. 'this is a number range'
-
preamble: String text to display before the field as an explanation - e.g. This number is limited to the range of 0 to 100. decimal steps (0.1) are allowed
-
units: String text displayed after the text input to denote units of measurement - e.g.'%'
-
required: Boolean is this field required - e.g. true
-
default: Number the default value of the field - e.g. '12'
-
step: Number The step amount that the input can increase or decrease by (HTML5) this also defines the HTML5 validation of number values - e.g. 0.1
Step works differently in different browsers. It is set in the input field and the browser may try to validate the number to the precision of a step. So if the step is 1 or not set you can only have whole numbers. |
-
max: Number if not using a range this defines the maximum value. You should use a range, this is here for backwards compatibility :-) - e.g. 200
-
min: Number if not using a range this defines the minimum value. You should use a range, this is here for backwards compatibility :-) - e.g. -20
question("Example1") {
"Number by a Range?" number: 0..100,
map: 'numberRange',
hint: 'this is a number range',
preamble: 'This number is limited to the range of 0 to 100. decimal steps (0.1) are allowed',
units: '%',
required: true,
default: '12',
step: 0.1
}
question("Example2") {
"Number by length?" number: 5,
map: 'numberLength',
hint: 'this is a 5 digit number',
preamble: "This number is limited to 200 with a minimum -20. However you'll get a validation error if you try 26.26 in html5 browsers",
required: true,
default: '12',
max: 200,
min: -20
}
3.2.3. Phone
The phone element allows numeric tel type input field. The entered value will be validated against a HTML5 phone number validator or use the phone number entry. You define the phone field with the number of numbers you expect,
e.g. "Mobile Phone" phone: 15
which will allow for spaces, dashes or country code for example +61 444 555 666.
It seems sensibly browsers are not validating the phone number, just changing the input method (e.g. on mobile browsers).
You can implement a phone number validator using the Form Validation or simply
use the regex pattern. You can use a regex like /[0-9\-\+ ]{8,15}/
to limit the size from a minimum 8 to max 15 digits
however that includes non digit characters like space, plus and minus. A custom validator is needed to confirm it’s a valid
phone number.
-
phone: Number max length of field - e.g. 15
-
map: String name of field in data map - e.g. 'phone1'
-
hint: String text to display as a hint to the user - e.g. 'this is a 15 digit phone number'
-
preamble: String text to display before the field as an explanation - e.g. 'This phone number allows spaces and + signs'
-
pattern: List a list containing a regex and an error message - e.g. [/[0-9\-\+ ]{8,15}/, You must have at least 8 numbers, spaces + or -]
-
sugest: String the name of a suggestion look up via the [Simple Suggestions plugin](http://nerderg.com/Simple+Suggestions+plugin) - e.g. 'phones'
-
required: Boolean is this field required - e.g. true
-
default: String the default value of the field - e.g. '+61419 555 666'
question("Example1") {
"Phone by length?" phone: 15,
map: 'phone1',
hint: 'this is a 15 digit phone number',
preamble: 'This phone number allows spaces and + signs',
required: true,
default: '+61419 555 666',
pattern: [/[0-9\-\+ ]{8,15}/, 'You must have at least 8 numbers, spaces + or --']
}
3.2.4. Money
The money form element displays a numeric text field with preceding money symbol. You define the length of the field in number of digits including the decimal point, e.g. money: 5 would allow 99.99 or 99999. You can define a default attribute like default: 0 to set the value if not already set.
-
money: Number The length of the field e.g. 6
-
map: String name of field in data map - e.g.'money1'
-
hint: String text to display as a hint to the user - e.g. 'this is a 6 digit money'
-
preamble: String text to display before the field as an explanation - e.g. 'This input is limited to $5000 with a minimum of $1.5'
-
required: Boolean is this field required - e.g. true
-
default: Number the default value of the field - e.g. '120.34'
-
step: Number [OPTIONAL] The step amount that the input can increase or decrease by (HTML5) this also defines the HTML5 validation of number values - e.g. 0.01
The step in money is set by default to 0.01, you can set this to override the default
|
Step works differently in different browsers. It is set in the input field and the browser may try to validate the number to the precision of a step. So if the step is 1 or not set you can only have whole numbers. |
-
max: Number the maximum value - e.g. 5000
-
min: Number the minimum value. - e.g. 1.5
question("Example1") {
"Money by length?" money: 6,
map: 'money1',
hint: 'this is a 6 digit money',
preamble: 'This input is limited to \$5000 with a minimum of \$1.5',
required: true,
default: '120.34',
max: 5000,
min: 1.5
}
3.2.5. Select
The select form element displays a select drop down using the defined choices, e.g. select: [one,two,three] which will display a drop down select of three items. You can define a default value and mapping.
This element is pretty limited consider using a text with suggestions.
-
select: List<String> a simple list of option strings - e.g. [one,two,three,four,five]
-
map: String name of field in data map - e.g.'selectme'
-
hint: String text to display as a hint to the user - e.g. 'select one of the options'
-
preamble: String text to display before the field as an explanation - e.g. 'Yep you can explain this with a preamble'
-
required: Boolean is this field required - e.g. true
-
default: Number the default value of the field - it should be from the list - e.g. 'three'
question("Example1") {
"Select something" select: ['one','two','three','four','five'],
map: 'selectme',
hint: 'select one of the options',
default: 'three',
preamble: 'Yep you can explain this with a preamble'
}
3.2.6. Attachment
The attachment form element defines a file upload element. You define the attachment element with the prefix of the filename uploaded. It also defines the key of the filename in the formdata Map;
e.g. "Attach any other documentation" attachment: "mydoc"
,
this will put the upload file name reference in the mydoc
map key. So for the example below the form data map will
contain:
"attachment1": {"mydoc": "test.mydoc-PeterMcNeil_CV-4.pdf"},
The attached file is uploaded to the location specified by uploaded.file.location
specified in your applications
config.groovy file.
By default uploaded files are stored in ./ which is usually the Tomcat home directory.
|
-
attachment: String the name of the document 'mydoc'
-
map: String name of field in data map - e.g.'attachment1'
-
hint: String text to display as a hint to the user - e.g. 'this is an attachment'
-
preamble: String text to display before the field as an explanation - e.g. 'An attachment lets you ask for documents to be attached and uploaded.'
-
required: Boolean is this field required - e.g. true
question("Example1") {
"Attachment?" attachment: 'mydoc',
map: 'attachment1',
hint: 'this is an attachment',
preamble: 'An attachment lets you ask for documents to be attached and uploaded.',
required: true
}
3.2.7. Date
The date form element defines a date input field with a calendar widget to select a date. The date element defines a date format to use,
e.g date: “d/M/yyyy”
.
Date can also have max and min attributes e.g max: today, min: 01/01/1910 to validate against. Minimum and Maximum values are inclusive. Currently dates don’t allow calculation e.g. today + 7. you’ll need to use a custom validation for that, see Form Validation.
Note currently you can’t set the date format to the users' local. |
-
date: String The date format to use - e.g. 'dd/MM/yyyy'
-
map: String name of field in data map - e.g.'date1'
-
hint: String text to display as a hint to the user - e.g. 'this is a dd/MM/yyyy date'
-
preamble: String text to display before the field as an explanation - e.g. 'This is a date limited to a maximum of todays date and minimum of 1/1/1900'
-
required: Boolean is this field required - e.g. true
-
default: Number the default value of the field - e.g. '23/3/1990'
-
max: Number the maximum value - e.g. today
-
min: Number the minimum value. - e.g. 1/1/1900
question("Example1") {
"Date?" date: 'dd/MM/yyyy',
map: 'date1',
hint: 'this is a dd/MM/yyyy date',
preamble: 'This is a date limited to a maximum of todays date and minimum of 1/1/1900',
required: true,
default: '23/3/1990',
max: 'today',
min: '1/1/1900'
}
3.2.8. Datetime
The datetime form element defines a date and time input fields with calendar widget and special time entry widget.
e.g. datetime: dd/MM/yyyy, min: 01/01/2012
Date can also have max and min attributes e.g max: today, min: 01/01/1910 to validate against. Minimum and Maximum values are inclusive.
Currently dates don’t allow calculation e.g. today + 7. you’ll need to use a custom validation for that, see Form Validation. |
Note currently you can’t set the date format to the users local. |
The time widget is broken into hours, minutes and AM/PM fields which can be set individually. typing a or p will change am/pm indication. The up and down arrow keys can advance/retard the time too.
-
datetime: String The date format to use - e.g.'dd/MM/yyyy'
-
map: String name of field in data map - e.g.'datetime1'
-
hint: String text to display as a hint to the user - e.g. 'this is a dd/MM/yyyy date with time input'
-
preamble: String text to display before the field as an explanation - e.g. 'This is a date limited to a minimum of todays date and maximum of 1/1/2014'
-
required: Boolean is this field required - e.g. true
-
default: Map Map containing the default date and time of the fields - e.g. [date: 23/3/1990, time: 10:00am]
-
max: Number the maximum value - e.g. 1/1/2014
-
min: Number the minimum value. - e.g. today
question("Example1") {
"Date and time?" datetime: 'dd/MM/yyyy',
map: 'datetime1',
hint: 'this is a dd/MM/yyyy date with time input. You can use your mouse wheel to change the time, just click and wheel.',
preamble: 'This is a date limited to a minimum of todays date and maximum of 1/1/2014',
required: true,
default: [date: '23/3/1990', time: '10:00am'],
max: '1/1/2014',
min: 'today'
}
3.2.9. Bool
The bool form element defines a boolean element. You do not need to specify any qualifier for the boolean element so just defining a question would make it a boolean element. e.g. “Do you like nuts?” map: isNuts defines a simple yes/no check box.
A Bool can also contain a subform (example 2). When it contains a subform the subform is displayed if the bool is checked.
Bool elements can be used with other elements, see Pick
-
map: String name of field in data map - e.g. 'yup'
-
hint: String text to display as a hint to the user - e.g. 'A simple boolean'
-
preamble: String text to display before the field as an explanation - e.g. 'This is a simple check box to indicate yes to a question.'
question("Example1") {
"Yes?" map: 'yup',
hint: 'A simple boolean',
preamble: 'This is a simple check box to indicate yes to a question.'
}
question("Example2") {
"Can I have more?" map: 'pleaseSir', {
"Soup" map: 'soup'
"Bread" map: 'bread'
"Other" text: 20, map: 'other'
}
}
3.2.10. Heading
The heading element just displays a heading at the point in the form it is used. You define this element with the heading level number,
e.g. “Lawyer preference” heading: 1
defines a <h1>
heading.
Because heading is not a data holding element you do not need a map:
attribute.
-
heading: Number heading number [1..6] -e.g. 1
question("Example1") {
"Text elements" heading: 1
}
3.2.11. Group
As we saw earlier the group form element just lets us group a set of elements as a set into a question. The group attribute defines the name for that group,
e.g. group: “names”
The group makes a named group in the form data map which contains the data for the elements in the group. For the example below the form data map would look like this
"Example1": {
"theLot": {
"text120": "hello is it me you are looking for?",
"text20": "hello",
},
},
You can nest groups within groups.
-
map: String name of field in data map - e.g. 'theLot'
-
hint: String text to display as a hint to the user - e.g. 'This is a group'
-
preamble: String text to display before the field as an explanation - e.g. 'This is a group of questions.'
question("Example1") {
"All elements" group: "theLot", hint: 'This is a group', {
"Text elements" heading: 1
"Text input 20?" text: 20,
map: 'text20',
hint: 'this is a 20 char text',
preamble: 'This is 20 char text with a pattern match so only letters can be entered',
pattern: [/[A-Za-z ]+/, 'You can only have letters'],
sugest: 'colour',
required: true,
default: 'hello'
"Text input 120?" text: 120,
map: 'text120',
required: true,
default: 'hello is it me you are looking for?'
}
}
3.2.12. Pick
The pick form element groups a set of boolean options. You can define it as pick: 1
or pick: “any”
. Pick : 1
makes a
radio button group that allows you to pick one of the options. pick: “any”
lets you create group of checkboxes.
Note when you have booleans in a pick 1 there is no map:
attribute on the Bool element because it is assigned
to the parent pick element. Because we use string names we need to ad brackets ()
after the boolean name e.g. "red"()
.
The form data map can be a little tricky to understand with pick
because of the way the data is handled in the form.
The following extract from the form data for the example below. You’ll notice the subform data for the pick1
element
has been moved outside the group to a new element name theLot_pick1
because a pick one only has a single value.
"Example1": {
"theLot": {
"pickany": {
"red": "Red",
"blue": {
"yes": "Blue",
"shade": "redish"
},
"green": "Green"
},
"pick1": "Blue",
},
"theLot_pick1": {"shade": "pale"}
}
-
pick: String the choice is 1 or any - e.g. '1'
-
map: String name of field in data map - e.g. 'pick1'
-
hint: String text to display as a hint to the user - e.g. 'this is a pick 1 group'
-
preamble: String text to display before the field as an explanation - e.g. 'Pick 1 lets you have a list of radio buttons. The radio button can expand when ticked to show more fields.'
question("Example1") {
"All elements" group: "theLot", hint: 'This is a group', {
"Pick one?" pick: '1',
hint: 'this is a pick 1 group',
preamble: 'Pick 1 lets you have a list of radio buttons. The radio button can expand when ticked to show more fields',
map: 'pick1', {
"Red"()
"Green"()
"Blue" {
"what shade?" text: 20, map: 'shade'
}
}
"Pick any?" pick: 'any',
hint: 'this is a pick any group',
preamble: 'Pick any lets you have a list of checkboxes. The checkboxes can expand when ticked to show more fields',
map: 'pickany', {
"Red" map: 'red'
"Green" map: 'green'
"Blue" map: 'blue', {
"what shade?" text: 20, map: 'shade'
}
}
}
}
3.2.13. Each
The each form element lets you repeat the contained form elements for each item in a list stored in the form data. This data could be set after another question by the rules engine. You can inject the item into the direct sub elements question string. For example:
question("M6") {
"fap benefits" each: "fap", {
"Does {fap} get a pension or benefit from Centrelink or the Department of Veterans Affairs?" map: 'getsBenefit', {
"Weekly income before tax?" money: 5,
default: "0",
hint: "If you're not sure put an approximate value here",
map: 'income'
"Centrelink Reference Number (CRN) or DVA reference number" text: 20,
map:'crn' //centre link number is alphanumeric
"Attach Centrelink consent form" attachment: "centrelinkConsent"
}
}
}
will substitute that name in the question.
To get the data for each of the people (faps) from the form data map you need to use a sanitised version of the persons name. To do this in the One Ring rules engine you should create a closure like this:
Closure fapName = { fap ->
String intermediate = fap.replaceAll(/[^a-zA-Z 0-9]/, '')
return intermediate.trim().replaceAll(' *', '_')
}
and use it in a rule like this:
rule('M8 Fap Health Care Card') {
def faps = fact.fap
faps.each { fap ->
def M8Q = fact.M8.fap[fapName(fap)]
if (M8Q?.yes && M8Q.healthCard == 'none') {
fact.require.add([Q: 'M8', message: "a copy of ${fap}'s health care or pensioner concession card."])
}
}
}
You need to add the list of things to iterate over into the form data map at the root level. This can be injected by the rules engine while processing the form Questions. In the above example this is done from names entered by the person filling out the form.
You can also inject them from the form controller. For the example form below you can do this by overriding getRuleFacts() which seeds the form data map when the form is created. This is the data sent to the Rules Engine when the forms' initial ruleset is called. See CreateForm.
package testForm
import com.nerderg.goodForm.FormController
class testFormController extends FormController {
protected static final String formName = 'test'
def createForm() {
createForm('test')
}
protected Map getRuleFacts() {
['lolly': ['sherbet', 'gum', 'chocolate', 'carrot stick']]
}
}
-
each: String The name of the form data map element that contains the data to iterate over - e.g. 'lolly'
-
hint: String text to display as a hint to the user - e.g. 'each repeats the question for each thing in a list'
-
preamble: String text to display before the field as an explanation - e.g. When you have a list of things you can dynamically add questions about those things with each.
question("Example1") {
"Dynamically repeated questions?" each: "lolly",
hint: "each repeats the question for each thing in a list",
preamble: "When you have a list of things you can dynamically add questions about those things with each.", {
"rate {lolly} out of ten" number: 0..10, map: 'rating'
}
}
3.2.14. ListOf
The listOf element lets you define a set of form elements that can be repeated or added as required by the user. This lets them add as many of these elements as required. the listOf attribute defines the name of the list,
e.g. listOf: “aliases”
.
listOf elements literally create a list of answers to the question, in the example below if the user provides more than
one things they love the form data map will have a Map of Lists under ways
related to each element in the subform.
"ways": {
"percentage": [
68,
23,
100
],
"loves": [
"eyes",
"nose",
"brain"
]
}
You can use GoodformService.groupList() to pivot that to a List of Maps for easy processing.
The mapped form data key is given by the listOf:
attribute so a map
attribute is not required.
-
listOf: String The name of the form data map element for the list - e.g. 'ways'
-
hint: String text to display as a hint to the user - e.g. 'listOf lets the user add form sections'
-
preamble: String text to display before the field as an explanation - e.g. When you want people to give you a variable number of answers, for example for all the people in a house hold use a listOf
question("Example1") {
"Let me count the ways" listOf: 'ways',
hint: 'listOf lets the user add form sections',
preamble: 'When you want people to give you a variable number of answers, for example for all the people in a house hold use a listOf', {
"I love your..." text: 30, map: 'loves'
"How much?" number: 0..100, map: 'percentage', units: '%'
}
}
4. Form data
GoodForm uses a Map to represent the form data. The form data map is stored and passed around as JSON to and from the rules engine.
The form data contains both answers and reference material inserted by the the Reference service, as well as meta data and error information. As the form data is a simple Map structure you can add information to it for reference. This make the form data a single canonical document.
Form data is added or removed by the rules engine, added by reference handlers, the FormController.getRuleFacts() inital data map and of course by the user answering questions. You can also pre-fill parts of the form via the rules engine and the reference service, just set the value of the question in the map in the rules engine.
Administrative metadata is also added during processing of the form such as:
"messages": [],
"fieldErrors": {},
"action": "next",
"require": [],
"controller": "jobApplicationForm",
"formVersion": 1
Here is an example of the form data map as JSON from the Job Application example project.
{
"Job4": {"role": {
"position": "Seated",
"company": "nerdErg Pty Ltd",
"jobNumber": "S123"
}},
"Job3": {"education": {
"highSchools": {
"name": "Hard Knocks",
"dateComplete": "1/12/1986"
},
"universities": {
"course": "Knowing Things",
"degree": "Knowing",
"name": "University of Knowledge",
"years": 4,
"dateComplete": "1/12/1990"
}
}},
"Job2": {"contact": {
"mobilePhone": "0418 555 656",
"homeAddress": "23 Blogs Pl\r\nBloggsville",
"workPhone": "2888 8888",
"postcode": 2255,
"homePhone": "6111 4555"
}},
"next": ["End"],
"Job1": {"names": {
"lastName": "McNeil",
"givenNames": "Peter",
"hasAlias": {"aliases": {
"alias": "",
"aliasType": ""
}},
"Title": "Mr"
}},
"instanceId": "1",
"Job6": {"references": {"referee": {
"lastName": "Bloggs",
"phone": "266556655",
"givenNames": "Joe"
}}},
"Job5": {"resume": {"resume": {"resume_file": "Job5.resume_file-PeterMcNeil_CV-4.pdf"}}},
"messages": [],
"fieldErrors": {},
"action": "next",
"require": [],
"controller": "jobApplicationForm",
"formVersion": 1
}
5. Services
GoodForm provides various services to assist in validation, referencing information from external sources, and managing the form life-cycle.
5.1. Form Validation
Good Form currently provides two basic forms of validation, field and form data.
Field validation is provided per field on the form and is not aware of the intent of the data or it’s relationship to other fields. Field validators know basic information such as if the field is a phone number or just a number.
You can supply a custom validator for a field that better understands the data type, such as a post(zip) code, and can validate it. Standard field validators are limited to that fields' data, but custom validators can access the current form data to validate against another field.
We recommend you think carefully about using cross field validation in custom field validators as most cross field validation belongs in business rules in the rules engine. |
Form data validation is done in the One Ring rules engine rules while deciding what the next set of questions should be. All the data so far is supplied to the rule set so it can cross check entered information and validate it against business rules.
5.1.1. Standard field validation
The Standard Form Elements contain basic field validators that will use HTML5 validation where it works. These include:
-
mandatory (required) fields,
-
data type
-
regex pattern validation,
-
number and date limits,
-
size limits
The validation limits and patterns are defined on a per field basis in the Form definition DSL.
5.1.2. Custom field validators
You can add custom field validators to Good Form by adding a hasError Closure to the formValidationService and adding
a validate: 'myValidator'
attribute to the field definition in the form DSL. You can
add the validator at any point you have access to the formValidationService, for example in the Bootstrap of your application
or in a @PostConstruct method of a Service.
The validator code is a Closure that you should think of as a hasError
function that returns true
if the field has
an error. The hasError
Closure takes the following parameters;
You might define a form and validator in Boostrap.groovy like this:
import com.nerderg.goodForm.FormDefinition
class BootStrap {
def formDataService
def formValidationService
def init = { servletContext ->
String postcodeFormDefinition = """
form {
question('Q1') {
"what is your postcode?" number: 4, map: 'pcode', validate: 'localPostcode'
}
}"""
if (!FormDefinition.findByName('postcodeForm')) {
formDataService.createNewFormVersion('postcodeForm', postcodeFormDefinition)
}
formValidationService.addCustomValidator("localPostcode") { FormElement formElement, Map formData, fieldValue, Integer index ->
List validCodes = [1234, 4567, 8901, 5555, 3333, 4576]
return !validCodes.contains(fieldValue)
}
...
}
}
In this example we’re checking fields with validate:
set to specific validator called localPostcode. When the
form section with that formElement is submitted the custom validator Closure is called to check the field. The example
checks to see if the value is in the set of valid postcodes, if not it returns true because it has an error.
You can use the supplied Form data map to look up other entered values. In this case you might check the postcode matches the suburb. Think carefully before doing cross field validation, because most of the time it belongs in a business rule in the rules engine.
You can add non-reserved attributes to your form dsl. For example you could do
this "what is your postcode?" number: 1000..9999, map: 'postcode', postcode: 'Australian' and check the postcode
attribute exists, and check it’s value.
|
You can replace a validator by calling addCustomValidator() again with the same name, or remove it using removeCustomValidator(name).
5.2. Reference service
The FormReferenceService
lets you look up data referred to in the form. The reference service is called on submission
of the form section before the rules engine is called to get the next set of questions. The reference data is attached to
the Form data map which is sent to the rules engine.
For example you may have a customer reference number. The reference service can look up the customer reference data and place that information in the form data Map. Once the data is in the Map the rules engine has access to it and can do something with it, like pre-fill some form data or affect the flow of the form to ask relevant questions.
To have a reference looked up you add a ref:
attribute to the form element in the form definition DSL. e.g.
question('email') {
"What is your email address?" text: 199, map: 'email', ref: 'customerEmail'
}
In this case we’re calling a reference service called customerEmail with the email address.
You add a reference service by passing the formReferenceService.addReferenceService('customerEmail') { ... }
method a
Closure that takes the field value.
import com.nerderg.goodForm.FormDefinition
class BootStrap {
def formDataService
def formReferenceService
def myCustomerDataService
def init = { servletContext ->
String emailFormDefinition = """
form {
question('email') {
"What is your email address?" text: 199, map: 'email', ref: 'customerEmail'
}
}"""
if (!FormDefinition.findByName('emailForm')) {
formDataService.createNewFormVersion('emailForm', emailFormDefinition)
}
formReferenceService.addReferenceService("customerEmail") { emailAddress ->
CustomerData customerData = myCustomerDataService.lookUpCustomerByEmail(emailAddress)
Map customerDataMap = [:]
... add data to the map
return customerDataMap
}
...
}
}
Your reference handler Closure should return a Map of data that is added to the Form data Map using the ref:
value.
So in the above example the data can be found at formData.customerEmail
.
5.3. Suggestion service (autocomplete)
The Simple Suggestions plugin provides the ability to add autocompletion to form elements, simply add the attribute suggest to the form definition (GoodForm DSL) in the element you want to provide autocomplete suggestions, and supply a corresponding text file in a suggestions sub-folder within your grails project.
question("Q2") {
"What is your favorite colour?" text: 20, suggest: "colour", map: 'faveColour'
}
5.4. Form data service
The FormDataService handles much of the form creation validation and general data handling tasks. In most cases you will
only ever need to call the getFormInstance()
method to process a completed form.
formDataService.getFormInstance(Long id)
gets the FormInstance object which holds the following information:
Date started //the date this form was created
String instanceDescription // eg first and last name
String formData //JSON representation of the form data map
String state //JSON list of the question sets that have been answered in the form
String userId //an identifier of the user that filled in this form
String currentQuestion //JSON list of question references that are to be displayed
FormVersion formVersion //The id of the form definition domain object related to this form
Date lastUpdated
Boolean readOnly //if this is true the form is read only and can't be edited.
Map storedFormData() //return the form data as a Map
List<List> storedState() // return the list of questions asked
boolean isAtEnd() //return true if the form is at the end.
...
The storedFormData() method returns the JSON data as a Map ready to process.
6. Form flow
GoodForm uses the One Ring rules engine to control the way a user moves through the set of questions defined in a form using the form DSL. The rules engine also validates the form data as the user submits it, cross referencing the data across fields and reference material.
As you can see from the sequence diagram below, the rules engine is called when a new form instance is created (before it is shown to the user), every time the user submits a set of answers, and at the end of the questions to check completeness.
The Rulesets that are called at the beginning and each time the user submits a set of answers, determine the next set of questions to ask the user by checking the answers given so far and any associated reference material.
6.1. Rulesets
6.2. Ruleset conventions
There are a set of conventions that govern the name of the ruleset called at a particular point in the form work flow.
When the form is created the rule set named the same as the formName is called with the initial form data, which is set
in the FormController.getRuleFacts()
method. You should override getRuleFacts()
to set the initial data for a form.
The [formName] Ruleset determines the first set of questions to ask.
When the form section (set of questions) is submitted the rule set called [formName][Last Question Reference] is called. for example if the last set of questions was ["Q1","Q2","Q4"] for the "JobApplication" form, the next rule set to be will be JobApplicationQ4.
When the ruleset sets next=['End']
it means there are no more questions to be asked. The ruleset named
[formName]CheckRequiredDocuments is called. e.g. JobApplicationCheckRequiredDocuments. This ruleset checks all the
required information has been supplied. The rule adds a message to the require
field in the fact map (for data) which
gives the question that requires filling in and a message. e.g.
require.add([Q: 'M8', message: "a copy of ${fap}'s health care or pensioner concession card."])
You may want the form not block entry of other data if a required field is not supplied at the time. An example of this is if you require supporting documents, like bank statements, that the person filling in the form may not have to hand. The user can finish filling in as much of the form as they can skipping the bits they don’t have, but when they finish the form the CheckRequiredDocuments rule set marks the required information and GoodForm provides links to go back and fill that info in. You can hook in to that and send an email reminding the person to supply that info before the form can be submitted. |
Rule sets must set the next:
element in the form data. For example the first three rule sets for a job application
might be:
ruleset("JobApplication") {
rule("First page of questions") {
when {
true
}
then {
next = ['Job1']
}
}
}
ruleset("JobApplicationJob1") {
require(['Job1']) //we need the answers to these questions
rule("Second page of job application questions") {
when {
true
}
then {
//add a message to the user
messages << "Hello $Job1.names.givenNames"
next = ['Job2']
}
}
}
ruleset("JobApplicationJob2") {
require(['Job1', 'Job2']) //we need the answers to these questions
rule("Third page of job application questions") {
when {
Job2.contact.homePhone.startsWith('02')
}
then {
next = ['Job3']
}
otherwise {
// add an error message to the field homePhone
if(fieldErrors['Job2.contact.homePhone0']) {
fieldErrors['Job2.contact.homePhone0'] += "/nHome phone must start with 02"
} else {
fieldErrors['Job2.contact.homePhone0'] = "Home phone must start with 02"
}
next = ['Job2']
}
}
}
6.2.1. Adding messages and errors from rules
See how in the ruleset "JobApplicationJob1" above we add a message "Hello $Job1.names.givenNames" to a messages set. This message is displayed at the top of the form for the user to see.
We also set an error by adding to the fieldErrors
map in "JobApplicationJob2" if the phone number doesn’t start with "02".
The name of the field includes the index into the [listOf] fields. So by default when there is no list the index
is 0. You can see that in the above example 'Job2.contact.homePhone0' .
|
6.3. One Ring rules engine
GoodForm talks to the One Ring rules engine service. The One Ring service runs in a tomcat container. You set the address of your One Ring server in your config.groovy file using the goodform.rulesEngine.uri key, e.g.
goodform.rulesEngine.uri = 'http://localhost:7070/rulesEngine'
Obviously you can set that on a per environment basis.
See the One Ring documentation for more details about setting up your One Ring service.
7. Customisation
GoodForm can be customised in a couple of ways to fit your application:
-
change the CSS
-
change the Javascript
-
edit the templates used to produce the form elements
-
add new form element types
7.1. Changing the CSS
The default GoodForm styles are contained in goodform.css
. You can override this as you normally would in your own CSS
files. GoodForm components live within a div.goodFormContainer
and the CSS is defined in terms of that container to prevent
stepping on toes.
The default GoodForm styles now rely on Bootstrap and Font Awesome.
7.2. Changing the Javascript
GoodForm Javascript makes the forms work as expected, including showing and hiding sections of the form. We’ve deliberately kept the Javascript to a minimum to maximise compatibility.
JQuery is required as well as Bootstraps JS.
7.3. Editing templates
GoodForm uses gsp templates to define the various form elements and pages it produces. You can edit these templates to your taste or add features.
To edit the templates run the install-goodform-templates script from you project directory:
grails install-goodform-templates
This will create a goodFormTemplates directory in your views directory containing a copy of all the templates used in GoodForm. The templates directory is broken into three directories, common, input, and display.
-
The common directory contains the templates for displaying the forms in different states and error messages etc.
-
The input directory contains the templates for all the form elements used when entering data.
-
The display directory contains the templates for all form elements when displaying the data. This is the representation of a completed form in a more compact way.
The templates in your views directory override the GoodForm defaults, so you can just change a single file in the templates and remove the rest from your projects view directory if you wish.
If you have to refer to a template that isn’t in your views/goodFormTemplates directory you will need to refer to the plugin. It’s often simpler to copy the templates in so they’re easier to reference when you need to refer to them. |
The input templates for a form element may be broken up into tops and tails or wrappers to allow nesting of other from elements.
7.4. Adding form element types
At some point you are going to want to add your favourite widget or a very specific form element to your forms. For this reason we have refactored GoodForm to be extensible.
Form elements need the following components:
-
a form element model - The data model is used to render a given element using a GSP template.
-
a form element tag closure - this takes the model and an output buffer and uses templates to render the element.
-
a input template
-
a display template
7.4.1. Form element model
The data model map contains the following elements by default:
-
String type - The element type, e.g. text, number, money etc.
-
String error - An error string for this element, if any.
-
String name - The elements name
-
String label - The label text to use on this element
-
String preamble - Preamble text if any, to be displayed with the element
-
Boolean required - If the element is required
-
String hint - The hint text if any
-
String prefix - Prefix text that goes just before the field e.g. "$"
-
String units - Units text that goes just after the field e.g. 'km/h"
-
Map fieldAttributes - fieldAttributes for the input element itself including the value, max, min, size, required in key/value pairs.
A simple form element model is a Closure that looks like this:
private Closure myThingModel = { FormElement e, Map answers, Integer index, Boolean disabled ->
getDefaultModelProperties(e, answers, index, disabled, [:])
}
You can add things to the model by including them in the map you pass to getDefaultModelProperties(), e.g.
private Closure dateModel = { FormElement e, Map answers, Integer index, Boolean disabled ->
String format = e.attr.date
getDefaultModelProperties(e, answers, index, disabled, [format: format, size: Math.max(format.size(), 10)])
}
And of course you can use the default model values to alter things before returning it:
private Closure listOfModel = { FormElement e, Map answers, Integer index, Boolean disabled ->
Map model = getDefaultModelProperties(e, answers, index, disabled, [:])
model.listSize = listSize(model.fieldAttributes.value)
return model
}
You add your model to GoodForm by calling goodFormService.addFormElementType(String name, Closure c)
with your closure.
So to add a new widget model using the defaults you might do this in your Bootstrap:
goodFormService.addFormElementType('widget') { FormElement e, Map answers, Integer index, Boolean disabled ->
getDefaultModelProperties(e, answers, index, disabled, [:])
}
7.4.2. Form element renderer closure
A Form element renderer is a closure that renders the form element to an output buffer, e.g.
private Closure wrapper = { Map model, Map attrs, Writer bufOut ->
goodFormService.render(template: "/goodFormTemplates/$attrs.templateDir/form_field_wrapper", model: model, out: bufOut)
}
The above example is the standard wrapper renderer which uses the formElement type to render an html input field with that type.
The $attrs.templateDir in the template path is required to change between input and display templates.
|
So lets say you want to add a new HTML5 input type called email you could simply add this to your Bootstrap.groovy:
def goodFormService
def init = { servletContext ->
goodFormService.addFormElementType('email') { FormElement e, Map answers, Integer index, Boolean disabled ->
goodFormService.getDefaultModelProperties(e, answers, index, disabled, [:])
}
goodFormService.addFormElementRenderer('email') { Map model, Map attrs, Writer bufOut ->
goodFormService.render(template: "/goodFormTemplates/$attrs.templateDir/form_field_wrapper", model: model, out: bufOut)
}
...
}
Then, in your views directory, add a template called goodFormTemplates/input/_type_email.gsp
that looks something like this:
<input type="email" name="${name}" id="${name}" ${raw(gf.addAttributes(fieldAttr: fieldAttributes, class: "form-control"))}/>
We need to add raw(...) around the gf.addAttributes(...) tag closure call because grails 2.4.3 will try and escape
the already escaped output. If you’re not using Grails 2.4.x raw() may not exist, and you don’t need to call it.
|
and a template called `goodFormTemplates/display/_type_email.gsp`that looks like this:
<gf:value value="${fieldAttributes.value}"/>
You would use your new element in a form definition like this:
...
"Enter your email address" email: 199,
map: 'emailAddress',
hint: 'this is an email input'
...
The rendered input code looks like this:
<div class="formField " title="">
<label for="test.theLot.emailAddress" class="">Enter your email address</label>
<input type="email" name="test.theLot.emailAddress" id="test.theLot.emailAddress" style="max-width: 141.8em" maxlength="199" value="peter@nerderg.com" class="form-control" />
<p class="hint help-block">this is an email input</p>
</div>
We may actually add an email element in the future. We haven’t yet because of the mixed responses different browsers have to the email type. If you are aiming at mobile browsers the email type works more consistently. |